<?php
/**
 * Created by 784855684@qq.com.
 * Link: https://github.com/lichtung/wshore
 * User: linzh
 * Date: 7/12/17
 * Time: 10:48 AM
 */
declare(strict_types=1);


namespace application\service\library;

use application\service\library\socket\EventHandlerInterface;
use application\service\library\socket\Message;
use application\service\library\socket\Passport;
use PHPSocketIO\SocketIO;
use PHPSocketIO\Socket;
use Workerman\Connection\TcpConnection;
use Workerman\Worker;
use wshore\library\Service;

class SocketServer extends Service
{
    /**
     * @var int
     */
    protected $port = null;
    protected $httpPort = null;
    /**
     * @var SocketIO
     */
    protected $socketio = null;
    /**
     * @var Worker
     */
    protected $httpio = null;
    /**
     * @var EventHandlerInterface
     */
    protected $handler = null;
    /**
     * @var Socket[]
     */
    protected $socketPool = [];

    /**
     * SocketServer constructor.
     * SocketServer constructor.
     * @param int $port 套接字端口
     */
    public function __construct(int $port = 1992)
    {
        parent::__construct($port);
        $this->port = $port;
        $this->httpPort = $port + 1;
        $this->socketio = new SocketIO($port);
    }


    public function setHandler(EventHandlerInterface $handler)
    {
        $this->handler = $handler;
    }

    /**
     * 计算连接池中的注册连接数量
     * @return int
     */
    public function countSocketPool(): int
    {
        return count($this->socketPool);
    }

    public function getkeys(): string
    {
        return implode(',', array_keys($this->socketPool));
    }

    /**
     * 返回连接
     * @param string $id
     * @return null|Socket
     */
    public function getSocket(string $id)
    {
        return $this->socketPool[$id]??null;
    }

    /**
     * 向连接池中添加套接字
     * @param Socket $socket
     * @return bool
     */
    public function addSocket(Socket $socket): bool
    {
        if ($socket->passport and $id = $socket->passport->id) {
            $this->socketPool[$id] = $socket;
            return true;
        } else {
            $socket->error = 'failed to add anonymous socket';
            return false;
        }
    }

    /**
     * 删除套接字连接
     * @param Socket $socket
     * @return bool
     */
    public function removeSocket(Socket $socket): bool
    {
        if ($socket->passport and $id = $socket->passport->id) {
            echo "client '{$id}' disconnect!\n";

            if (isset($this->socketPool[$id])) {
                unset($this->socketPool[$id]);
                return true;
            } else {
                $socket->error = 'socket id not exist';
                return false;
            }
        } else {
            echo "client 'nick' disconnect!\n";
            $socket->error = 'failed to remove anonymous socket';
            return false;
        }
    }

    /**
     * 向套接字发送失败的消息
     * @param Socket $socket
     * @param string $message 为空时自动从socket中获取错误信息
     * @param int $code
     * @return void
     */
    public static function emitError(Socket $socket, string $message = '', int $code = 0)
    {
        $message or $message = $socket->error;
        $socket->emit('message', [
            'status' => $code,
            'message' => $message,
        ]);
        echo "Error:{$message}\n";
    }

    /**
     * 向套接字发送成功的消息
     * @param Socket $socket
     * @param string $message
     * @return void
     */
    public static function emitSuccess(Socket $socket, string $message = 'OK')
    {
        $socket->emit('message', [
            'status' => 1,
            'message' => $message,
        ]);
        echo "S:{$message}\n";
    }


    /**
     * 开启进程
     * @return void
     */
    public function start()
    {
        $context = &$this;
        # 监听套接字
        $this->socketio->on('connect', function (
            /** @var Socket $socket */
            $socket) use (
            /** @var SocketServer $context */
            &$context,
            /** @var Socket[] $socketPool */
            &$socketPool
        ) {
            //处理连接事件
            if ($context->handler->connect($socket, $context)) {
                self::emitSuccess($socket, 'connect success');

                # 注册身份
                $socket->on('register', function ($info) use (
                    /** @var Socket */
                    $socket,
                    /** @var SocketServer */
                    &$context,
                    /** @var Socket[] */
                    &$socketPool
                ) {
                    if (is_array($info)) {
                        if ($socket->passport) {
                            self::emitError($socket, 'a passport has registered', -1);
                        } else {
                            if (!empty($info['id'])) {
                                if ($context->handler->register(new Message($info), $socket, $context)) {
                                    # 关闭已经存在的连接
                                    if ($sockectExist = $context->getSocket($info['id'])) {
                                        self::emitError($sockectExist, 'a new connection replace this connection', -10);
                                        unset($sockectExist->passport);
                                    }

                                    # 创建和添加passport资料
                                    $passport = new Passport($info['id'], $info['pwd']??'');
                                    $socket->passport = $passport;

                                    # 添加连接，失败时推送错误信息并且
                                    if ($context->addSocket($socket)) {
                                        $keys = $context->getkeys();
                                        self::emitSuccess($socket, "register {$info['id']} success : $keys ");
                                    } else {
                                        $keys = $context->getkeys();
                                        self::emitError($socket, "register {$info['id']} failed :" . $socket->error . "\n keys:$keys");
                                    }

                                } else {
                                    self::emitError($socket, 'register deny', -3);
                                }
                            } else {
                                self::emitError($socket, 'identify is required for registeration', -2);
                            }
                        }
                    } else {
                        self::emitError($socket, 'please emit object ');
                    }
                });

                //客户端发送消息时执行
                $socket->on('message', function ($message) use ($socket, &$context) {
                    if (is_array($message)) {
                        if ($socket->passport) {
                            $context->handler->message(new Message($message), $socket, $context);
                            echo "receive message from unregistered client\n";
                        } else {
                            self::emitError($socket, 'deny message from unregistered client');
                        }
                    } else {
                        self::emitError($socket, 'please emit object ');
                    }
                });

                # 断开连接
                $socket->on('disconnect', function () use (
                    /** @var Socket */
                    $socket,
                    /** @var SocketServer */
                    &$context,
                    /** @var Socket[] */
                    &$socketPool
                ) {
                    if ($socket->passport) {
                        $context->handler->disconnect($socket, $context);
                        $context->removeSocket($socket);
                    } else {
                        unset($socket);
                        echo "an unregister client disconnect\n";
                    }
                });

            } else {
                # 客户端会在一段时间内自动发起重新连接的操作，这将导致更加浪费服务器资源，所以不进行unset($socket)的操作
                self::emitError($socket, 'access deny');
            }
        });
        # 允许web服务发送消息(允许后台或者前段发送一次性的消息)
        # 问题：如果直接把listenHttp放到闭包的外面，将无法共享context中的连接池
        $this->socketio->on('workerStart', function () use ($context) {

            $context->httpio = new Worker('http://0.0.0.0:' . $context->httpPort);
            /**
             * http服务监听到连接时回调用
             * @param TcpConnection $connection
             * @param array $data
             * @return void
             */
            $context->httpio->onMessage = function (TcpConnection $connection, array $data) use ($context) {
                try {
                    if (true !== $context->handler->request($connection, $data, $context)) {
                        # 请求参数
                        $get = $data['get']??[];
                        $post = $data['post']??[];
                        $request = $post ? array_merge($get, $post) : $get;

                        # 请求功能
                        switch ($request['function']??'') {
                            # 转发功能 示例：http://localhost:1993/?function=transmit&to=linzh&from=linzh&content=thisiscontent
                            case 'transmit':
                                if (!empty($request['to']) and !empty($request['from'])) {
                                    if ($socket = $context->getSocket($request['to'])) {
                                        $socket->emit('message', $request);
                                        $connection->send(json_encode([
                                            'status' => 1,
                                            'message' => "transmit finished",
                                        ]));
                                    } else {
                                        $connection->send(json_encode([
                                            'status' => 0,
                                            'message' => "socket '{$request['to']}' offline,transmit failed",
                                            'list' => array_keys($context->socketPool), # 当前在线
                                        ]));
                                    }
                                } else {
                                    $connection->send(json_encode([
                                        'status' => 0,
                                        'message' => 'transmit require to,from ',
                                    ]));
                                }
                                break;
                            # 当前在线
                            case 'list':
                                $connection->send(json_encode([
                                    'status' => 1,
                                    'list' => array_keys($context->socketPool),
                                ]));
                                break;
                        }
                    }
                    $connection->close();
                } catch (\Throwable $throwable) {
                    $error = $throwable->getMessage();
                    echo "Error:{$error}\n";
                }
            };
            // 执行监听
            $context->httpio->listen();
        });

        # 开启套接字服务进程和http服务进程
        Worker::runAll();
    }


}